package gamebackend;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;

import framework.Color;
import framework.GameContext;
import framework.GraphicsWrapper;
import framework.Point;
import framework.ResourcesWrapper;
import framework.Screen;
import gamebackend.LaserShip.BlockField;
import gamebackend.Ship.ShipListener;
import generators.BasicBlockGenerator;
import generators.BlockGenerator;
import generators.BlockRule;
import generators.DoorGenerator;
import generators.EmbiggenGenerator;
import generators.EndCapGenerator;
import generators.HorizontalStreakGenerator;
import generators.PipeGenerator;
import generators.ReplaceGenerator;
import generators.ScaredGenerator;
import generators.SoloHorizontalKiller;
import generators.SquareGenerator;
import generators.StarGenerator;
import generators.VerticalStreakGenerator;
import gui.BlockStyle;
import gui.SpecialMeter;

/**
 * Represents and controls the players current run in the game.  Paths, block layers,
 * and generators are added or removed by the GameSession.  This class is responsible for
 * achieving a pleasant difficulty curve as the session progresses. 
 * @author Jonathan Caddey
 *
 */
public class GameSession extends Screen implements ShipListener, BlockField {
	public static final int LASER_SHIP = 0;
	public static final int CLONE_SHIP = 1;
	
	public static final int BOARD_WIDTH = 240;
	public static final int BOARD_HEIGHT = 400;
	private static final int EXTRA_HEIGHT = 200;
	
	public static final int KEY_LEFT = 1;
	public static final int KEY_RIGHT = 2;
	public static final int KEY_UP = 3;
	public static final int KEY_DOWN = 4;
	public static final int KEY_SPECIAL = 5;
	public static final int KEY_PAUSE = 6;
	/**
	 * How many times longer the height is from the width.
	 */
	public static final float ASPECT_RATIO = (float) BOARD_HEIGHT / BOARD_WIDTH;
	
	private static boolean SHOW_PREDICTORS = false;
	private static final int KEY_TEST_BOOST = 38;
	
	
	private static final int POWER_UP_INTERVAL = 700;
	private static final int POWER_UP_MIN_SPACE = 50;
	/**
	 * The distance between opportunities for modifying
	 * generators and paths.  This is adhered to for
	 * determinism.
	 */
	private static final double FIXED_DISTANCE_INTERVAL = 5;
	/**
	 * How close two power ups can be side by side
	 */
	private static final int POWER_UP_DISTANCE = 70;
	/**
	 * How many power ups must appear on a hard path before one appears
	 * on an easy path.
	 */
	private static final int POWER_UP_MIN_SKIP = 5; // used to be 3
	
	// must be a power of 2
	private static final int POWER_UPS_PER_BAG = 4;
	
	private final long my_seed;
	private int my_stage;
	/**
	 * Width, in game space.
	 */
	private final int my_width;
	/**
	 * Height, in game space.
	 */
	private final int my_height;
	private int my_previousWidth;
	private int my_previousHeight;
	private double my_scale;
	private double my_xOffset;
	private double my_yOffset;
	private final int my_extraHeight;
	/**
	 * TODO The game speeds up until this score is reached.  used for debugging,
	 * should be unchangeable on production if someone has a memory editor.
	 */
	private int my_targetScore = 0;
	
	
	/**
	 * How far from the bottom of the screen the ship appears.
	 */
	private final double my_shipYMax;
	
	private final Random my_random;
	
	private Flier my_flier;
	private final List<FlightPath> my_paths;
	private FlightPath my_easyPath;
	private FlightPath my_hardPath;
	
	private final Map<FlightPath, Double> my_pathLifeTimes;
	private List<EnviroLaser> my_enviroLasers;
	private final Ship my_ship;
	
	private List<BlockLayer> my_layers;
	private BlockLayer my_blockLayer;
	private BlockLayer my_stars;
	private Biome my_biome;
	
	
	private List<BlockPart> my_blockParts;
	
	
	private double my_distance;
	private final SpecialMeter my_special;
	/**
	 * Used for FIXED_DISTANCE_INTERVAL.
	 */
	private double my_fixedDistanceLeft;
	
	
	
	// powerups
	private List<PowerUp> my_powerUps;
	private List<PowerUpPredictor> my_predictors;
	private int my_powerUpTimer;
	/**
	 * How many power ups appeared on the hard path since a power up appeared on an easy path.
	 */
	private int my_powerUpEasySkip;
	private double[] my_powerUpIntervals;
	private int my_powerUpIndex;
	
	
	private BlockLayer my_midGround;
	private boolean my_paused = false; // TODO
	private boolean my_focused;
	
	private int my_time;
	
	private final FlierListener my_listener;
	
	public static void setPredictors(boolean showPredictors) {
		SHOW_PREDICTORS = showPredictors;
	}
	public GameSession(final GameContext the_game, final FlierListener the_listener, final Class<? extends Ship> the_shipClass) {
		super(the_game);
		Class<? extends Ship> t = LaserShip.class;
		ResourcesWrapper res = my_game.getResourcesWrapper();
		BlockStyle.ASTEROID.initialize(res);
		BlockStyle.METAL.initialize(res);
		BlockStyle.THORN.initialize(res);
		PowerUp.initialize(res);
		
		
		my_seed = System.currentTimeMillis(); 
		System.out.println("Seed: " + my_seed);
		my_random = new Random(my_seed);
		my_width = BOARD_WIDTH;
		my_height = BOARD_HEIGHT;
		my_shipYMax = my_height / 6; // used to be 10
		my_extraHeight = EXTRA_HEIGHT;
		
		my_listener = the_listener;
		
		my_flier = new MultiFlier();
		my_paths = new ArrayList<FlightPath>();
		my_pathLifeTimes = new HashMap<FlightPath, Double>();
		
		my_enviroLasers = new ArrayList<EnviroLaser>();
		
		if (the_shipClass == LaserShip.class) {
			LaserShip.initialize(res);
			my_ship = new LaserShip(this, this, my_width / 2, my_height - my_shipYMax);
		} else {
			CloneShip.initialize(res);
			my_ship = new CloneShip(this,  my_width / 2, my_height - my_shipYMax);
		}
		my_easyPath = new FlightPath(Block.PRIMARY_PATH, my_extraHeight, 30, my_ship.getCopy());
		my_hardPath = new FlightPath(Block.SECONDARY_PATH, my_extraHeight, 10, my_ship.getCopy());
		addPath(my_easyPath, 0);
		addPath(my_hardPath, 0);
		
		my_stars = new BlockLayer(my_width, my_height, 0, 120, 200, 50);
		my_stars.addGenerator(new StarGenerator(new Block(BlockStyle.PLAIN, new Color(185, 185, 185)), new Random(0), .3));
		my_stars.addGenerator(new BigStarGenerator(new Random(0), 1f / 130));
		
		my_stars.step(0, my_height);
		my_stars.fixedStep(0, my_height);
		my_layers = new ArrayList<BlockLayer>();
		
		BlockLayer layer = new BlockLayer(my_width, my_height, my_extraHeight, 15, 16, Collections.unmodifiableList(my_paths), 15 * 16);
		
		my_midGround = new BlockLayer(my_width, my_height, my_extraHeight, 15, 16, Collections.unmodifiableList(my_paths), 15 * 16);
		

		my_blockLayer = layer;
		my_layers.add(layer);
		
		my_biome = new StartBiome(this);
		
		my_blockParts = new ArrayList<BlockPart>();
		
		
		my_powerUps = new ArrayList<PowerUp>();
		my_predictors = new ArrayList<PowerUpPredictor>();
		
		my_powerUpIntervals = new double[POWER_UPS_PER_BAG];
		my_powerUpIndex = POWER_UPS_PER_BAG;
		my_powerUpTimer = POWER_UP_INTERVAL;
		
		
		my_midGround.step(0,my_height + my_extraHeight);
		my_midGround.fixedStep(0,my_height + my_extraHeight);
		for (BlockLayer b : my_layers) {
			b.step(0, my_height + my_extraHeight);
			b.fixedStep(0, my_height + my_extraHeight);
		}
		
		for (int i = 0; i < 3; i++) {
			my_hardPath.boost();
			my_powerUps.add(new PowerUp(PowerUp.Type.SPEED, my_width / 2, i * POWER_UP_DISTANCE));
		}
		final PowerUp added = new PowerUp(PowerUp.Type.SPEED, my_width / 2, 0);
		my_powerUps.add(added);
		my_special = new SpecialMeter(my_ship.getSpecialSegmentCount());
		my_special.setSize(68, 9);
		my_special.setPosition(0, 16 + 4);
		
	}
	
	public void setFocused(boolean focus) {
		my_focused = focus;
		if (!my_focused) {
			my_paused = true;
		}
	}
	public void unpause() {
		my_paused = false;
	}
	
	
	public void step(final int the_timeDelta) {
		if (my_paused) {
			return;
		}
		final int timeDelta;
		if (my_targetScore > 0) {
			if (my_targetScore > getScore()) {
				timeDelta = the_timeDelta * 30;
			} else {
				timeDelta = (int)(the_timeDelta * .6);
			}
		} else {
			timeDelta = the_timeDelta;
		}
		
		if (my_ship.isAlive()) {
			my_time += timeDelta;
		}
		
		final double cameraYChange; 
		final double previousX = my_ship.getXPosition();
		
		cameraYChange = my_ship.getMinSpeed() * timeDelta +
				(my_height - my_ship.getYPosition() - my_shipYMax) * timeDelta * 4 / 1000; // TODO magic number
		if (my_ship.isAlive()) {
			my_distance += my_ship.getYPositionChange(timeDelta);
		}
		my_ship.step(timeDelta, cameraYChange);
		// check bounds on ship
		if (my_ship.getXPosition() < 0) {
			my_ship.setXPosition(0);
		} else if (my_ship.getXPosition() > my_width - 1) {
			my_ship.setXPosition(my_width - 1);
		}
		
		my_stars.step(timeDelta, cameraYChange / 11);//was 7
		my_stars.fixedStep(timeDelta, cameraYChange / 11);
		
		
		
		for (Iterator<BlockPart> it = my_blockParts.iterator(); it.hasNext();) {
			BlockPart part = it.next();
			part.step(timeDelta, cameraYChange);
			if (!part.isAlive()) {
				it.remove();
			}
		}
		my_fixedDistanceLeft += cameraYChange;
		while (my_fixedDistanceLeft > 0) {
			fixedStep(timeDelta, FIXED_DISTANCE_INTERVAL, previousX);
			my_fixedDistanceLeft -= FIXED_DISTANCE_INTERVAL;
			
		}
		
		
		
		for (BlockLayer layer : my_layers) {
			layer.step(0, cameraYChange);
			// the hit detection code below could be moved to fixed step.
			if (my_ship.isAlive() && layer.pointCollides(my_ship.getXPosition(), my_height - my_ship.getYPosition())) {
				boolean hitSide = !layer.pointCollides(previousX, my_height - my_ship.getYPosition());
				final boolean destroy;
				if (hitSide) {
					destroy = my_ship.collidedSide();
				} else {
					destroy = my_ship.collidedBottom();
				}
				if (destroy) {					
					List<BlockPart> parts = layer.collideAndDestroy(my_ship.getXPosition(), my_height - my_ship.getYPosition());
					my_blockParts.addAll(parts);
				}
			}
		}
		my_midGround.step(timeDelta, cameraYChange);
		
		
		for (Iterator<PowerUp> it = my_powerUps.iterator(); it.hasNext();) {
			PowerUp p = it.next();
			p.step(timeDelta, cameraYChange);
			final boolean hit = my_ship.isAlive() && p.collides(my_ship.getXPosition(), my_ship.getYPosition());
			
				
			if (p.getY() > my_height + 30) {
				my_ship.missPowerUp(p);
				it.remove();
			}
			if (hit) {
				my_ship.hitPowerUp(p);
				
				it.remove();
			}
			
		}
		
		
		
		
		for (Iterator<EnviroLaser> it = my_enviroLasers.iterator(); it.hasNext();) {
			final EnviroLaser laser = it.next();
			laser.step(cameraYChange);
			if (laser.isDangerous()) {
				for (BlockLayer layer : my_layers) {
					my_blockParts.addAll(layer.checkRectangle(laser.getLeft(), laser.getTop(),
							laser.getWidth(), laser.getHeight()));
				}
				my_ship.checkEnviroLaser(laser.getLeft(), laser.getWidth());
			}
			if (!laser.isAlive()) {
				it.remove();
			}
			
		}
		
		
		for (Iterator<PowerUpPredictor> it = my_predictors.iterator(); it.hasNext();) {
			if (!it.next().isAlive()) {
				it.remove();
			}
		}
		
		
		
	}
	
	private double my_hashTimer;
	/**
	 * This portion of step must be done with fixed deltaTimes to ensure the same
	 * seed reproduces the same course.
	 * @param deltaTime
	 */
	private void fixedStep(final int timeDelta, final double cameraYChange, final double previousX) {
		my_hashTimer -= cameraYChange;
		if (my_hashTimer < 0) {
			int random = my_random.nextInt(900) + 100;
			my_hashTimer += random;
			System.out.print(random % 10);
		}
		// countdown on paths
		for (FlightPath path : my_pathLifeTimes.keySet()) {
			my_pathLifeTimes.put(path,
					my_pathLifeTimes.get(path) - cameraYChange);
		}
		// check to see if any paths died
		if (!my_pathLifeTimes.isEmpty()) {
			for (Iterator<FlightPath> it = my_paths.iterator(); it.hasNext();) {
				final FlightPath path = it.next();
				if (my_pathLifeTimes.containsKey(path)
						&& my_pathLifeTimes.get(path) <= 0) {
					// TODO in order to make this deterministic, paths should probably
					// manage their own length durations
					if (path.finish()) {
						it.remove();
						my_pathLifeTimes.remove(path);
					}
				}
			}
		}
		my_flier.step(cameraYChange);
		for (FlightPath path : my_paths) {
			path.step(cameraYChange);
		}
		
		
		for (BlockLayer layer : my_layers) {
			layer.fixedStep(timeDelta, cameraYChange);
		}
		my_midGround.fixedStep(timeDelta, cameraYChange);
		if (!my_biome.isAlive()) {
			final int stages = 6;
			final int tier = Math.min(2, my_stage / stages
					+ (my_random.nextDouble() < .2 ? 1 : 0));
			System.out.println("Stage: " + my_stage + " Tier: " + tier);
			final double length = 4000 + 500 * my_stage;
			
			/*/test biomes
//			my_biome = new CanyonBiome(this, 0, my_random, length * 1.3);
//			my_biome = new LaserCrystalBiome(this, my_ship, my_layers.get(0), 0, my_random, 912000);
//			my_biome = new SnakeBiome(this, tier, my_random, length + 20000);
//			my_biome = new GateBiome(this, tier, my_random, length + 20000);
			my_biome = new TestBiome(this, 2, my_random, 999999);
			/*/
			switch (my_stage % stages) {
			case 0:
				if (tier > 0 && my_random.nextInt(2) == 1) {
					my_biome = new ButterCaveBiome(this, tier, my_random, length);
				} else {
					my_biome = new AsteroidBiome(this, tier, my_random, length);
				}
				break;
			case 1:
				my_biome = new GateBiome(this, tier, my_random, length / 3);
				break;
			case 2:
				my_biome = new SnakeBiome(this, tier, my_random, length + 2000);
				break;
			case 3:
				if (my_random.nextDouble() > .5) {
					//my_biome = new CrateBiome(this, tier, my_random, length);
					//my_biome = new CrystalBiome(this, tier, my_random, length);
					my_biome = new LaserCrystalBiome(this, my_ship, my_layers.get(0), tier, my_random, length);
				} else {
					my_biome = new LaserCrystalBiome(this, my_ship, my_layers.get(0), tier, my_random, length);
				}
				break;
			case 4:
				my_biome = new GateBiome(this, tier + 1, my_random, length / 3);
				break;
			case 5:
				my_biome = new CanyonBiome(this, tier, my_random, length * 1.3);
				break;
			}
			//*/
			
			my_stage++;
		}
		my_biome.step(cameraYChange);
		for (final EnviroLaser laser : my_enviroLasers) {
			laser.fixedStep(cameraYChange);
		}
		
		
		
		
		
		
		if (my_powerUpTimer <= 0) {
			if (my_powerUpIndex == POWER_UPS_PER_BAG) {
				my_powerUpIndex = 0;
				// space reserved so that the minimum space is between each power up.
				double padding = POWER_UP_MIN_SPACE * POWER_UPS_PER_BAG;
				my_powerUpIntervals[0] = POWER_UPS_PER_BAG * POWER_UP_INTERVAL;
				for (int i = 1; i < POWER_UPS_PER_BAG; i <<= 1) {
					for (int j = i - 1; j >= 0; j--) {
						final double random = my_random.nextDouble() * (my_powerUpIntervals[j] - padding);
						my_powerUpIntervals[2 * j + 1] = random + padding / 2;
						my_powerUpIntervals[2 * j] = my_powerUpIntervals[j] - (random + padding / 2);
					}
					padding /= 2;					
					
				}
			}
			my_powerUpTimer += my_powerUpIntervals[my_powerUpIndex];
			my_hardPath.boost();
			final PowerUp added = new PowerUp(PowerUp.Type.SPEED, my_hardPath.getXPosition(), my_hardPath.getFirstY() - my_extraHeight);
			my_predictors.add(new PowerUpPredictor(added, my_width, Color.RED));
			my_powerUps.add(added);
			my_powerUpIndex ++;
			
			my_powerUpEasySkip++;
			
			if (Math.abs(my_easyPath.getXPosition() - my_hardPath.getXPosition()) > POWER_UP_DISTANCE) {
				final double chance = my_random.nextDouble();
				if (my_powerUpEasySkip > POWER_UP_MIN_SKIP) {
					if (my_ship.getHealth() == my_ship.getMaxHealth()) {
						PowerUp extra = new PowerUp(chance < .3 ? PowerUp.Type.DECOY : PowerUp.Type.SPEED,
								my_easyPath.getXPosition(), added.getY());
						my_powerUps.add(extra);
						my_predictors.add(new PowerUpPredictor(extra, my_width, Color.RED));
					} else {
						PowerUp extra = new PowerUp(PowerUp.Type.REPAIR, my_easyPath.getXPosition(), added.getY());
						my_powerUps.add(extra);
						my_predictors.add(new PowerUpPredictor(extra, my_width, Color.GREEN));
					}
					
					my_powerUpEasySkip -= POWER_UP_MIN_SKIP + 1;
				}
			}
//			System.out.println("Easy skip " + my_powerUpEasySkip);
		}
		my_powerUpTimer -= cameraYChange;
	}
	public int getWidth() {
		return my_width;
	}
	public int getHeight() {
		return my_height;
	}
	public int getTotalHeight() {
		return my_height + my_extraHeight;
	}
	/**
	 * @return the y position where the ship prefers to be--though the ship may be above
	 * or below depending on boosts or collisions.
	 */
	public double getPreferredShipYPosition() {
		return my_shipYMax;
	}
	private void addPath(FlightPath path, double length) {
		my_paths.add(path);
		my_pathLifeTimes.put(path, length);
	}
	
	@Override
	public void keyPressed(final int the_code) {
		setControlState(the_code, true);
	}
	@Override
	public void keyReleased(final int the_code) {
		setControlState(the_code, false);
	}
	/**
	 * Keeps track whether certain keys are down.
	 * @param code the key code of the key.
	 * @param pressed whether the key is pressed (true) or released (false).
	 */
	public void setControlState(int code, boolean pressed) {
		switch(code) {
		case KEY_LEFT:
			my_ship.leftThruster(pressed);
			
			break;
		case KEY_RIGHT:
			my_ship.rightThruster(pressed);
			break;
		
		case KEY_TEST_BOOST:
			if (pressed) {
			}
			break;
		case KEY_SPECIAL:
			my_ship.special(pressed);
			break;
		case KEY_PAUSE:
			my_paused = true;
			my_listener.onPause();
			break;
		}
		
		
	}
	
	
	
	
	/*
	 * This could be just addHazard in the future if
	 * I know how to deal with handling collisions.
	 */
	public void addEnviroLaser(final EnviroLaser the_enviroLaser) {
		my_enviroLasers.add(the_enviroLaser);
	}
	
	public void render() {
		final GraphicsWrapper g = my_game.getGraphicsWrapper();
		// TODO I have a resize method.  Should every screen have a resize method?
		g.setColor(Color.GRAY);
		g.fillRect(0, 0, g.getWidth(), g.getHeight());
		boolean resize = false;
		if (g.getWidth() != my_previousWidth) {
			my_previousWidth = g.getWidth();
			resize = true;
		}
		if (g.getHeight() != my_previousHeight) {
			my_previousHeight = g.getHeight();
			resize = true;
		}
		if (resize) {
			resize(g.getWidth(), g.getHeight());
		}
		//g.saveMatrix();
	    g.translate(my_xOffset, my_yOffset);
	    g.scale(my_scale, my_scale);
	    
		
	    //g2.setStroke(new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
	    
	    //draw play area
	    g.setColor(Color.BLACK);
	    g.fillRect(0, 0, BOARD_WIDTH, BOARD_HEIGHT);
	    
	    // draw top of map
//	    g.setColor(Color.GREEN);
//		g.fillRect(0, -my_extraHeight, my_width, 10);
		
	    
	    
		// draw stars
	    my_stars.render(g);
	    
	    if (my_paused) {
	    	g.setColor(new Color(0, 0, 0, (int) (.8 * 255)));
	    	g.fillRect(0, 0, my_width, my_height);
//	    	String s = "Click then move to unpause";
//	    	final Rectangle bounds = g.getStringBounds(s);
//	    	g.setColor(Color.WHITE);
//	    	g.drawString(s, (int) (my_width - bounds.getWidth()) / 2, (int) (my_height * 1 / 3 - bounds.getHeight() / 2));
	    	
	    } else {	    
			my_midGround.render(g);
			
			// TODO this threw concurrent modification exception when I had
			// interval 200.
			// I should catch the exception or use a thread safe list.
			for (PowerUp p : my_powerUps) {
				p.render(g);
			}
			
					
			// draw obstacles
			for (BlockLayer layer : my_layers) {
				layer.render(g);
			}
			

			
			// TODO this threw concurrent modification exception when I made a
			// lot of block parts..
			for (BlockPart part : my_blockParts) {
				part.render(g);
			}
			
			
			// draw hazards
		    for (Hazard h : my_enviroLasers) {
		    	h.render(g);
		    }
			
			// draw predictors
			if (SHOW_PREDICTORS) {
				for (Iterator<PowerUpPredictor> it = my_predictors.iterator(); it.hasNext();) {
					PowerUpPredictor p = it.next();
					p.render(g);
				}
			}
			
		}
	    // draw the ship
	    my_ship.render(g);
	    
	    
	    
	    // draw score
	    g.setColor(Color.WHITE);
	    g.drawString("Score: " + getScore(), my_width - 80, 16);//TODO actually measure here at some point
	    final int s = my_time / 1000 % 60;
	    final int m = my_time / 1000 / 60;
	    g.drawString("Time: " + (m < 10 ? "0" : "") + m
	    	+ (s < 10 ? ":0" : ":") + s, my_width - 80, 32);
	    int health = my_ship.getHealth();
	    if (health >=3) {
			g.setColor(Color.GREEN);
		} else if (health >= 2) {
			g.setColor(Color.YELLOW);
		} else {
			g.setColor(Color.RED);
		}
	    if (health > 0) {
	    	g.drawString("Health: " + health, 0, 16);
	    	
	    	// draw special ability bar
	    	my_special.setPercent(my_ship.getSpecialPercent());
	    	my_special.render(g);
	    }
	    
		
		// draw path
//		for (FlightPath path : my_paths) {
//			path.render(g);
//		}
		
//		g.restoreMatrix();
//		KInput input = my_game.getInput();
//		if (input != null) {
//			List<TouchEvent> events = input.getTouchEvents();
//			if (!events.isEmpty()) {
//				g.setColor(Color.RED);
//				TouchEvent event = events.get(0);
//				g.fillOval(event.x, event.y, 10, 10);
//			}
//		}
		
		
		
		
		
		
		
		
		
		
		
		/* this is the last of stuff from cavnas
		
		
		long time = System.currentTimeMillis() - 17;
	    int fps = 1000 / ((int) (time - my_lastPaintTime));
	    
	    my_avgFps = .1 * fps + .9 * my_avgFps;
	    
	    my_fpsDisplaySkip --;
	    if (my_fpsDisplaySkip < 0) {
	    	my_fpsDisplaySkip += FPS_DISPLAY_FRAME_SKIP;
	    	my_fps = (int) my_avgFps;
	    }
	    g2.setColor(Color.WHITE);
	    g2.drawString("FPS " + my_fps, 0, 10);
	    my_lastPaintTime = time;
	    */
	}
	public int getScore() {
		return (int) (my_distance / 10);
	}
	/**
	 * This probably will be its own class later.
	 */
	public abstract class Biome {
		/*
		 * Needs to be able to add:
		 * flight paths
		 * backgrounds
		 * midgrounds
		 * TODO I'm pretty sure I'm doing bad OOP.  Lots of these
		 * getters could be moved to Gamesession, which biome
		 * children apparently will already have access to.
		 */
		private final GameSession my_session;
		private double my_passed;
		private int my_tier;
		private final double my_length;
		private final Random my_random;
		public Biome(GameSession the_session, final int the_tier, Random the_random, double the_length) {
			my_session = the_session;
			my_length = the_length;
			my_random = the_random;
			my_tier = the_tier;
		}
		public void step(double y_change) {
			my_passed += y_change;
		}
		public boolean isAlive() {
			return my_passed < my_length;
		}
		public void addFlightPath(FlightPath the_path, double the_length) {
			addPath(the_path, the_length);
		}
		public void setFlier(Flier the_flier) {
			my_flier = the_flier;
		}
		public void addEasyPath(FlightPath the_easyPath, double the_length) {
			addPath(the_easyPath, the_length);
			my_easyPath = the_easyPath;
		}
		public void setEasyPath(FlightPath the_easyPath) {
			my_easyPath = the_easyPath;
		}
		public void addHardPath(FlightPath the_hardPath, double the_length) {
			addPath(the_hardPath, the_length);
			my_hardPath = the_hardPath;
		}
		public void setHardPath(FlightPath the_hardPath) {
			my_hardPath = the_hardPath;
		}
		public FlightPath getEasyPath() {
			return my_easyPath;
		}
		public FlightPath getHardPath() {
			return my_hardPath;
		}
		public void addGenerator(BlockGenerator the_generator, double the_length) {
			my_blockLayer.addGenerator(the_generator, the_length);
		}
		public void addToMidGround(BlockGenerator the_generator, double the_length) {
			my_midGround.addGenerator(the_generator, the_length);
		}
		public GameSession getGameSession() {
			return my_session;
		}
		public Random getRandom() {
			return my_random;
		}
		public double getLength() {
			return my_length;
		}
		public double getCovered() {
			return my_passed;
		}
		public double getRemaining() {
			return getLength() - getCovered();
		}
		public int getExtraHeight() {
			return my_extraHeight;
		}
		public int getWidth() {
			return getGameSession().getWidth();
		}
		public int getTier() {
			return my_tier;
		}
		
	}
	
	
	public interface FlierListener {
		public void onGameEnded(final int the_time, final int the_score, final long the_seed);
		public void onPause();
	}
	
	public void resize(final int the_width, final int the_height) {
	    final float ratio = (float) the_height / the_width;
	    // multiply by in game coordinates to get pixels.
	    
	    /*
	     If the screen is much wider than it is tall, the
	     ship is very close to the bottom of the screen.
	     
	     If the screen is much taller than it is wide,
	     the ship is farthest from the bottom of the screen.
	     
	     If the screen is in between, there are no margins
	     top or sides, and the ship is variable distance
	     to the bottom of the screen.
	     */
	    final double shipHeight = my_height - my_shipYMax *.7;
	    if (ratio > (float) my_height / my_width) {
	    	my_scale = (float) the_width / my_width;
	    	my_xOffset = 0;
	    	my_yOffset = (the_height - my_scale * my_height);// / 2;
	    } else if (ratio < (float) shipHeight / my_width) {
	    	my_scale = (float) the_height / shipHeight;
	    	my_yOffset = 0;
	    	my_xOffset = (the_width - my_scale * my_width) / 2;
	    } else {
	    	my_scale = (float) the_width / my_width;
	    	my_xOffset = 0;
	    	my_yOffset = 0;
	    }
	    
//	    if (ratio >= ASPECT_RATIO) {
//	    	my_scale = (float) the_width / BOARD_WIDTH;
//	    	my_xOffset = 0;
//	    	my_yOffset = (the_height - my_scale * my_height) / 2;
//	    } else {
//	    	my_scale = (float) the_height / BOARD_HEIGHT;
//	    	my_yOffset = 0;
//	    	my_xOffset = (the_width - my_scale * my_width) / 2;
//	    }
	}
	public void setTarget(final double the_target) {
		double target = (the_target - my_xOffset) / my_scale;
		my_ship.moveTo(target);
	}
	public void setDesiredVelocityRatio(final double the_ratio) {
		my_ship.setDesiredVelocityRatio(the_ratio);
	}
	
	@Override
	public void dispose() {
		for (BlockLayer layer : my_layers) {
			layer.dispose();
		}
		my_midGround.dispose();
		BlockStyle.ASTEROID.dispose();
		BlockStyle.METAL.dispose();
		BlockStyle.THORN.dispose();
	}
	@Override
	public void onShipDestroyed() {
		my_listener.onGameEnded(my_time, getScore(), my_seed);
	}
	@Override
	public int checkRectangleAndDestroy(double the_x, double the_y,
			double the_width, double the_height) {
		int hits = 0;
		for (BlockLayer layer : my_layers) {
			final List<BlockPart> parts = layer.checkRectangle(the_x, the_y,
					the_width, the_height);
			my_blockParts.addAll(parts);
			hits += parts.size();
		}
		return hits;
	}
	
	/**
	 * To be called when the mouse or finger is released.
	 */
	public void onTouchUp() {
		my_ship.touchUp();
	}
}
